package cn.demoncat.util.exec;

import cn.demoncat.util.exception.IoRuntimeException;
import cn.demoncat.util.io.CloseUtil;
import cn.demoncat.util.io.FileUtil;
import cn.demoncat.util.io.IoUtil;
import cn.demoncat.util.io.entity.LsFile;
import cn.demoncat.util.lang.ThreadUtil;
import cn.demoncat.util.lang.constant.CharsetConstant;
import cn.demoncat.util.lang.entity.LineReadCallback;
import cn.demoncat.util.lang.entity.LineReadCallback.LineReadFinishException;
import com.jcraft.jsch.*;

import java.io.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Vector;

/**
 * SSH 远程执行工具
 * 
 * @author 延晓磊
 *
 * @since 2019年6月13日
 */
public class SshUtil {

	/**
	 * 创建SSH连接
	 *
	 * @param username
	 * @param password
	 * @param ip
	 * @param port		端口，默认22
	 *
	 * @return
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static Session connect(String username, String password, String ip, Integer port) {
		return connect(username, password, ip, port, 0);
	}
	
	/**
	 * 创建SSH连接
	 * 
	 * @param username
	 * @param password
	 * @param ip
	 * @param port		端口，默认22
	 * @param timeout 	连接超时毫秒
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static Session connect(String username, String password, String ip, Integer port, Integer timeout) {
		// 默认端口
		if (port == null) {
			port = 22;
		}
		// 创建JSch
		JSch jsch = new JSch();
		Session session;
		try {
			// 创建Session
			session = jsch.getSession(username, ip, port);
			// 第一次登陆的时不提示
			session.setConfig("StrictHostKeyChecking", "no");
			// 密码
			session.setPassword(password);
			// 读取超时：Socket.SO_TIMEOUT 0=无限读取，非0=读取超时抛异常中断(未关闭连接，可以再次读取)
			session.setTimeout(0);
			// 创建连接：Socket.CONNECTION_TIMEOUT 0=阻塞连接，非0=重试时间（循环尝试连接，直到超时）
			session.connect(timeout);
		} catch (JSchException e) {
			throw new IoRuntimeException("SSH连接失败：ip = " + ip, e);
		}
		return session;
	}

	/**
	 * 关闭SSH连接
	 * <pre>
	 * 场景1：一次性连接 == 执行操作后自动关闭
	 * 场景2：非一次性连接 == 获取session; try{ 执行操作 }finally{ 关闭连接 }
	 * </pre>
	 * @param session
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static void close(Session session) {
		if (session != null) {
			session.disconnect();
		}
	}

	/**
	 * 执行一条Shell命令
	 * <pre>
	 * 注：只能执行一条指令，如果要执行多条，可以使用ChannelShell进行交互指令，或通过如下方式编写指令
	 * 1）命令之间用 ; 隔开：各个命令都会执行，但不保证每个命令都执行成功
	 * 2）命令之间用 && 隔开：前面的命令执行成功，才会去执行后面的命令
	 * 3）命令之间用||隔开：只有前面的命令执行失败后才去执行下一条命令，直到执行成功一条命令为止
	 * </pre>
	 * @param session
	 * @param command	一条Shell命令
	 * @param onceSession	是否为单次连接：true 默认，使用后关闭连接；false 不自动关闭连接，待try-finally中主动关闭
	 * 
	 * @return 命令输出
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static List<String> exec(Session session, String command, boolean onceSession) {
		ChannelExec channel = null;
		try {
			// 创建exec通道：只能执行一条指令
			channel = (ChannelExec) session.openChannel("exec");
			// 设置命令
			channel.setCommand(command);
			channel.setPty(true);
			channel.setErrStream(System.err);
			channel.setInputStream(null);
			// 连接
			channel.connect();
			// 读取执行结果
			return IoUtil.readToStrs(channel.getInputStream());
		} catch (Exception e) {
			throw new IoRuntimeException("执行Shell命令失败：command = " + command, e);
		} finally {
			// 释放资源
			if (channel != null) {
				channel.disconnect();
			}
			// 使用后关闭连接
			if (onceSession) {
				close(session);
			}
		}
	}
	
	/**
	 * 执行一条Shell命令，使用后关闭连接
	 * 
	 * @param session
	 * @param command	一条Shell命令
	 * 
	 * @return 命令输出
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static List<String> exec(Session session, String command) {
		return exec(session, command, true);
	}

	/**
	 * 执行一条Shell命令
	 * 
	 * @param session	
	 * @param callback		响应读取器
	 * @param command		一条Shell命令
	 * @param onceSession	是否为单次连接：true 默认，使用后关闭连接；false 不自动关闭连接，待try-finally中主动关闭
	 * @param timeout		读取超时：毫秒，用于自动中断读取，默认0/null（无限）
	 * 
	 * @author 延晓磊
	 * @since 2019年6月13日
	 */
	public static void exec(Session session, LineReadCallback callback, String command, boolean onceSession, Long timeout) {
		ChannelExec channel = null;
		try {
			// 创建exec通道：只能执行一条指令
			channel = (ChannelExec) session.openChannel("exec");
			// 设置命令
			channel.setCommand(command);
			channel.setPty(true);
			channel.setErrStream(System.err);
			channel.setInputStream(null);
			// 连接
			channel.connect();
			// 无限超时
			if (timeout == null || timeout < 1) {
				// 直接读取结果
				readExec(channel, callback, command);
			}else {
				// 启动子线程读取
				Thread thread = new Thread(new ExecThread(channel, callback, command));
				thread.start();
				// 同步等待子线程
				ThreadUtil.join(thread, timeout);
				// 停止读取，完成并退出子线程
				channel.disconnect();
			}
		} catch (JSchException e) {
			throw new IoRuntimeException("执行Shell命令失败：command = " + command, e);
		} finally {
			// 释放资源
			if (channel != null) {
				channel.disconnect();
			}
			// 使用后关闭连接
			if (onceSession) {
				close(session);
			}
		}
	}
	
	/**
	 * 执行一条Shell命令，使用后关闭连接
	 * 
	 * @param session	
	 * @param callback	响应读取器
	 * @param command	一条Shell命令
	 * @param timeout	读取超时：毫秒，用于自动中断读取，默认0/null（无限）
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static void exec(Session session, LineReadCallback callback, String command, Long timeout) {
		exec(session, callback, command, true,timeout);
	}
	
	/**
	 * sftp列表目录
	 * 
	 * @param session
	 * @param dir			目录
	 * @param onceSession	是否为单次连接：true 默认，使用后关闭连接；false 不自动关闭连接，待try-finally中主动关闭
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static List<LsFile> sftpList(Session session, String dir, boolean onceSession) {
		ChannelSftp channel = null;
		try {
			// 创建sftp通信通道
			channel = (ChannelSftp) session.openChannel("sftp");
			// 连接
			channel.connect();
			// 列表目录
			Vector<?> ls = channel.ls(dir);
			List<LsFile> list = new ArrayList<>(ls.size());
			if (!ls.isEmpty()) {
				LsFile lsFile;
				String line;
				for (Object file : ls) {
					line = file.toString();
					lsFile = LsFile.create(dir, line);
					if (LsFile.isFile(lsFile.getName())) {
						list.add(lsFile);
					}
				}
				list.sort(Comparator.comparing(LsFile::getType).thenComparing(LsFile::getName));
			}
			return list;
		} catch (Exception e) {
			throw new IoRuntimeException("列表目录失败：dir = " + dir, e);
		} finally {
			// 释放资源
			if (channel != null) {
				channel.disconnect();
			}
			// 使用后关闭连接
			if (onceSession) {
				close(session);
			}
		}
	}
	
	/**
	 * sftp列表目录，使用后关闭连接
	 * 
	 * @param session
	 * @param dir			目录
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static List<LsFile> sftpList(Session session, String dir) {
		return sftpList(session, dir, true);
	}
	
	/**
	 * sftp上传文件
	 * 
	 * @param session
	 * @param fileInput		文件
	 * @param fileName		文件上传路径名
	 * @param onceSession	是否为单次连接：true 默认，使用后关闭连接；false 不自动关闭连接，待try-finally中主动关闭
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static void sftpUpload(Session session, InputStream fileInput, String fileName, boolean onceSession) {
		ChannelSftp channel = null;
		try {
			// 创建sftp通信通道
			channel = (ChannelSftp) session.openChannel("sftp");
			// 连接
			channel.connect();
			// 上传文件
			IoUtil.copy(fileInput, channel.put(fileName));
		} catch (Exception e) {
			throw new IoRuntimeException("上传文件失败：fileName = " + fileName, e);
		} finally {
			// 释放资源
			if (channel != null) {
				channel.disconnect();
			}
			// 使用后关闭连接
			if (onceSession) {
				close(session);
			}
		}
	}

	/**
	 * sftp上传文件，使用后关闭连接
	 * 
	 * @param session
	 * @param fileInput		文件
	 * @param fileName		文件上传路径名
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static void sftpUpload(Session session, InputStream fileInput, String fileName) {
		sftpUpload(session, fileInput, fileName, true);
	}
	
	/**
	 * sftp上传文件
	 * 
	 * @param session
	 * @param filePath		文件本地路径
	 * @param fileName		文件上传路径名
	 * @param onceSession	是否为单次连接：true 默认，使用后关闭连接；false 不自动关闭连接，待try-finally中主动关闭
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static void sftpUpload(Session session, String filePath, String fileName, boolean onceSession) {
		// 上传文件
		sftpUpload(session, FileUtil.getIns(new File(filePath)), fileName, onceSession);
	}

	/**
	 * sftp上传文件，使用后关闭连接
	 * 
	 * @param session
	 * @param filePath		文件本地路径
	 * @param fileName		文件上传路径名
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static void sftpUpload(Session session, String filePath, String fileName) {
		// 上传文件
		sftpUpload(session, FileUtil.getIns(new File(filePath)), fileName, true);
	}
	
	/**
	 * sftp下载文件
	 * 
	 * @param session
	 * @param fileName		文件下载路径名
	 * @param fileOut		文件
	 * @param onceSession	是否为单次连接：true 默认，使用后关闭连接；false 不自动关闭连接，待try-finally中主动关闭
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static void sftpDownload(Session session, String fileName, OutputStream fileOut, boolean onceSession) {
		ChannelSftp channel = null;
		try {
			// 创建sftp通信通道
			channel = (ChannelSftp) session.openChannel("sftp");
			// 连接
			channel.connect();
			// 下载文件
			IoUtil.copy(channel.get(fileName), fileOut);
		} catch (Exception e) {
			throw new IoRuntimeException("下载文件失败：fileName = " + fileName, e);
		} finally {
			// 释放资源
			if (channel != null) {
				channel.disconnect();
			}
			// 使用后关闭连接
			if (onceSession) {
				close(session);
			}
		}
	}
	
	/**
	 * sftp下载文件，使用后关闭连接
	 * 
	 * @param session
	 * @param fileName		文件下载路径名
	 * @param fileOut		文件
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static void sftpDownload(Session session, String fileName, OutputStream fileOut) {
		sftpDownload(session, fileName, fileOut, true);
	}

	/**
	 * sftp下载文件
	 * 
	 * @param session
	 * @param fileName		文件下载路径名
	 * @param filePath		文件本地路径
	 * @param onceSession	是否为单次连接：true 默认，使用后关闭连接；false 不自动关闭连接，待try-finally中主动关闭
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static void sftpDownload(Session session, String fileName, String filePath, boolean onceSession) {
		// 下载文件
		sftpDownload(session, fileName, FileUtil.getOut(new File(filePath)),onceSession);
	}
	
	/**
	 * sftp下载文件，使用后关闭连接
	 * 
	 * @param session
	 * @param fileName		文件下载路径名
	 * @param filePath		文件本地路径
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	public static void sftpDownload(Session session, String fileName, String filePath) {
		sftpDownload(session, fileName, filePath, true);
	}

	
	/**
	 * 线程Exec
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月14日
	 */
	public static class ExecThread implements Runnable{
		// 通道
		private final ChannelExec channel;
		// 响应读取器
		private final LineReadCallback callback;
		// 命令
		private final String command;

		public ExecThread(ChannelExec channel, LineReadCallback callback, String command) {
			super();
			this.channel = channel;
			this.callback = callback;
			this.command = command;
		}

		@Override
		public void run() {
			// 读取命令响应
			readExec(channel, callback, command);
		}
	}
	
	/**
	 * 读取执行响应
	 * 
	 * @param channel		exec通道
	 * @param callback		响应读取器
	 * @param command		一条Shell命令
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年6月13日
	 */
	private static void readExec(ChannelExec channel, LineReadCallback callback, String command) {
		BufferedReader reader = null;
		try {
			// 读取执行结果
			reader = new BufferedReader(new InputStreamReader(channel.getInputStream(), CharsetConstant.UTF8_CHARSET));
			String line = reader.readLine();
	        while (line != null) {
	        	// 可读
	        	if (callback.getStatus()) {
	        		callback.read(line);
	 	            line = reader.readLine();
				}else {
					break;
				}
	        }
		} catch (LineReadFinishException e) {
			// 停止读取
		} catch (Exception e) {
			throw new IoRuntimeException("读取Shell执行结果失败：command = " + command, e);
		} finally {
			// 释放资源
			CloseUtil.close(reader);
			channel.disconnect();
		}
	}
	
	
}
